/*
 * Copyright 2009-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License i distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.paoding.rose.jade.context;

import net.paoding.rose.jade.annotation.DAO;
import net.paoding.rose.jade.annotation.SQLParam;
import net.paoding.rose.jade.annotation.SQLType;
import net.paoding.rose.jade.cache.JadeCache;
import net.paoding.rose.jade.constant.JadeConstant;
import net.paoding.rose.jade.dataaccess.DataAccessFactory;
import net.paoding.rose.jade.excetion.JadeException;
import net.paoding.rose.jade.rowmapper.RowMapperFactory;
import net.paoding.rose.jade.statement.DAOMetaData;
import net.paoding.rose.jade.statement.JdbcStatement;
import net.paoding.rose.jade.statement.Querier;
import net.paoding.rose.jade.statement.SelectQuerier;
import net.paoding.rose.jade.statement.Statement;
import net.paoding.rose.jade.statement.StatementMetaData;
import net.paoding.rose.jade.statement.StatementWrapperProvider;
import net.paoding.rose.jade.statement.UpdateQuerier;
import net.paoding.rose.jade.statement.cached.CacheProvider;
import net.paoding.rose.jade.statement.cached.CachedStatement;
import net.paoding.rose.jade.statement.interpreter.Interpreter;
import net.paoding.rose.jade.statement.interpreter.factory.InterpreterFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * @author 王志亮 [qieqie.wang@gmail.com]
 */
public class JadeInvocationHandler implements InvocationHandler {

    private static final Log logger = LogFactory.getLog(JadeInvocationHandler.class);

    private final JadeCache<Method, Statement> jadeCache;

    private final DAOMetaData daoMetaData;

    private final DataAccessFactory dataAccessFactory;

    private final RowMapperFactory rowMapperFactory;

    private final InterpreterFactory interpreterFactory;

    private final CacheProvider cacheProvider;

    private final StatementWrapperProvider statementWrapperProvider;

    @SuppressWarnings({"FieldCanBeLocal", "unused"})
    private final ClassLoader classLoader;

    public JadeInvocationHandler(
            ClassLoader classLoader,
            DAOMetaData daoMetaData,
            InterpreterFactory interpreterFactory,
            RowMapperFactory rowMapperFactory,
            DataAccessFactory dataAccessFactory,
            CacheProvider cacheProvider,
            StatementWrapperProvider statementWrapperProvider,
            BeanFactory beanFactory) {
        this.classLoader = classLoader;
        this.daoMetaData = daoMetaData;
        this.rowMapperFactory = rowMapperFactory;
        this.dataAccessFactory = dataAccessFactory;
        this.interpreterFactory = interpreterFactory;
        this.cacheProvider = cacheProvider;
        this.statementWrapperProvider = statementWrapperProvider;
        //noinspection unchecked
        this.jadeCache = (JadeCache<Method, Statement>) beanFactory.getBean("jadeMethodStatementCache", JadeCache.class);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        final boolean debugEnabled = logger.isDebugEnabled();
        if (debugEnabled) {
            logger.debug("invoking " + daoMetaData.getDAOClass().getName() + "#" + method.getName());
        }

        // 调用object的方法
        if (method.getDeclaringClass() == Object.class) return invokeObjectMethod(proxy, method, args);

        // 获取当前DAO方法对应的Statement对象
        Method targetMethod = proxy.getClass().getMethod(method.getName(), method.getParameterTypes());
        Optional<Statement> optional = jadeCache.get(targetMethod);
        Statement statement = optional.orElseGet(() -> getStatement(targetMethod, method));
        //
        // 将参数放入  Map
        Map<String, Object> parameters;
        StatementMetaData statementMetaData = statement.getMetaData();
        if (args == null || args.length == 0) return statement.execute(new LinkedHashMap<>(4), args);

        parameters = new LinkedHashMap<>(args.length * 2 + 4);
        for (int i = 0; i < args.length; i++) {
            parameters.put(String.format(":%s", i + 1), args[i]);
            SQLParam sqlParam = statementMetaData.getSQLParamAt(i);
            if (Objects.nonNull(sqlParam)) {
                if (Objects.equals(sqlParam.value(), JadeConstant.CONNECTION_INDEX_FLAG)) {
                    throw new JadeException("SQLParam value equals {},Please set other values!", JadeConstant.CONNECTION_INDEX_FLAG);
                }
                parameters.put(sqlParam.value(), args[i]);
            }
            String value = statementMetaData.getMethodParamNameAt(i);
            if (Objects.isNull(value) || parameters.containsKey(value)) continue;
            if (Objects.equals(value, JadeConstant.CONNECTION_INDEX_FLAG)) {
                throw new JadeException("Param name equals {},Please set other values!", JadeConstant.CONNECTION_INDEX_FLAG);
            }
            parameters.put(value, args[i]);
        }
        return statement.execute(parameters, args);
    }

    private Statement getStatement(Method targetMethod, Method method) {
        synchronized (this) {
            Optional<Statement> optional = jadeCache.get(targetMethod);
            if (optional.isPresent()) return optional.get();
            StatementMetaData smd = new StatementMetaData(daoMetaData, method);
            SQLType sqlType = smd.getSQLType();
            Querier querier = sqlType == SQLType.READ
                    ? new SelectQuerier(dataAccessFactory, smd, rowMapperFactory.getRowMapper(smd))
                    : new UpdateQuerier(dataAccessFactory, smd);
            Interpreter interpreter = interpreterFactory.getInterpreter(smd);
            Statement statement = new JdbcStatement(smd, sqlType, interpreter, querier);
            if (Objects.nonNull(cacheProvider)) statement = new CachedStatement(cacheProvider, statement);
            jadeCache.put(targetMethod, wrap(statement));
            return statement;
        }
    }

    private Statement wrap(Statement statement) {
        return Objects.nonNull(statementWrapperProvider) ? statementWrapperProvider.wrap(statement) : statement;
    }

    private Object invokeObjectMethod(Object proxy, Method method, Object[] args) throws CloneNotSupportedException {
        String methodName = method.getName();
        if ("toString".equals(methodName)) {
            return JadeInvocationHandler.this.toString();
        }
        if ("hashCode".equals(methodName)) {
            return daoMetaData.getDAOClass().hashCode() * 13 + this.hashCode();
        }
        if ("equals".equals(methodName)) {
            return args[0] == proxy;
        }
        if ("clone".equals(methodName)) {
            throw new CloneNotSupportedException("clone is not supported for jade dao.");
        }
        throw new UnsupportedOperationException(daoMetaData.getDAOClass().getName() + "#"
                + method.getName());
    }

    @Override
    public String toString() {
        DAO dao = daoMetaData.getDAOClass().getAnnotation(DAO.class);
        return daoMetaData.getDAOClass().getName() + "[catalog=" + dao.catalog() + "]";
    }

}
